﻿using FastData.Caching;
using FastData.Mapping;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace FastData
{
    internal class SelectQueryTranslator<T> : ExpressionVisitor
    {
        private DataAccess _data;
        private SelectQuery _selectQuery;
        private string _singleMethod;
        private string _commandText;

        public string BufferedText
        {
            get
            {
                var text = _commandText;
                _commandText = string.Empty;
                return text;
            }
        }

        internal SelectQueryTranslator(DataAccess data)
        {
            _data = data;
        }

        internal IEnumerable<T> Translate(Expression expression)
        {
            _selectQuery = new SelectQuery(_data);

            this.Visit(expression);

            return _selectQuery.Execute<T>();
        }

        internal T TranslateSingle(Expression expression)
        {
            var result = Translate(expression);

            if (_singleMethod == "First")
            {
                return result.First();
            }
            if (_singleMethod == "FirstOrDefault")
            {
                return result.FirstOrDefault();
            }
            if (_singleMethod == "Single")
            {
                return result.Single();
            }
            if (_singleMethod == "SingleOrDefault")
            {
                return result.SingleOrDefault();
            }

            throw new NotSupportedException(string.Format("The method '{0}' is not supported", _singleMethod));
        }

        internal async Task<IEnumerable<T>> TranslateAsync(Expression expression)
        {
            _selectQuery = new SelectQuery(_data);

            this.Visit(expression);

            return await _selectQuery.ExecuteAsync<T>();
        }

        private static Expression StripQuotes(Expression e)
        {
            while (e.NodeType == ExpressionType.Quote)
            {
                e = ((UnaryExpression)e).Operand;
            }

            return e;
        }

        protected override Expression VisitMethodCall(MethodCallExpression m)
        {
            if (m.Method.DeclaringType == typeof(Queryable))
            {
                if (m.Method.Name == "Where")
                {
                    this.Visit(m.Arguments[0]);

                    LambdaExpression lambda = (LambdaExpression)StripQuotes(m.Arguments[1]);
                    this.Visit(lambda.Body);

                    _selectQuery.Where(BufferedText);

                    return m;
                }
                else if (m.Method.Name == "FirstOrDefault" || m.Method.Name == "First")
                {
                    _singleMethod = m.Method.Name;
                    this.Visit(m.Arguments[0]);

                    _selectQuery.Take(1);

                    return m;
                }
                else if (m.Method.Name == "SingleOrDefault" || m.Method.Name == "Single")
                {
                    _singleMethod = m.Method.Name;
                    this.Visit(m.Arguments[0]);

                    return m;
                }
                else if (m.Method.Name == "OrderBy")
                {
                    this.Visit(m.Arguments[0]);

                    LambdaExpression lambda = (LambdaExpression)StripQuotes(m.Arguments[1]);
                    this.Visit(lambda.Body);

                    _selectQuery.OrderBy(BufferedText);

                    return m;
                }
                else if (m.Method.Name == "Take")
                {
                    this.Visit(m.Arguments[0]);
                    this.Visit(m.Arguments[1]);

                    _selectQuery.Take(int.Parse(BufferedText));

                    return m;
                }
                else if (m.Method.Name == "Skip")
                {
                    this.Visit(m.Arguments[0]);
                    this.Visit(m.Arguments[1]);

                    _selectQuery.Skip(int.Parse(BufferedText));

                    return m;
                }
            }
            else if (m.Method.DeclaringType == typeof(string))
            {
                if (m.Method.Name == "Contains")
                {
                    Visit(m.Object);
                    string left = BufferedText;
                    Visit(m.Arguments[0]);
                    string right = BufferedText;

                    _commandText += "(CHARINDEX(" + right + "," + left + ")>0)";

                    return m;
                }
                else if (m.Method.Name == "IsNullOrEmpty")
                {
                    Visit(m.Arguments[0]);
                    string text = BufferedText;

                    _commandText += "((" + text + " IS NULL) OR (" + text + "=''))";

                    return m;
                }
            }
            else if (m.Method.DeclaringType == typeof(Extension))
            {
                if (m.Method.Name == "Cache")
                {
                    this.Visit(m.Arguments[0]);

                    var value = ((ConstantExpression)m.Arguments[1]).Value;
                    if (value is TimeSpan)
                    {
                        _selectQuery.Cache((TimeSpan)value);
                    }
                    else if (value is DateTime)
                    {
                        _selectQuery.Cache((DateTime)value);
                    }
                    return m;
                }
                else if (m.Method.Name == "CacheDependency")
                {
                    this.Visit(m.Arguments[0]);

                    _selectQuery.CacheDependency((TableDependency)((ConstantExpression)m.Arguments[1]).Value);

                    return m;
                }
            }

            throw new NotSupportedException(string.Format("The method '{0}' is not supported", m.Method.Name));
        }

        protected override Expression VisitUnary(UnaryExpression u)
        {
            switch (u.NodeType)
            {
                case ExpressionType.Not:
                    _commandText += " NOT ";
                    this.Visit(u.Operand);
                    break;

                case ExpressionType.Convert:
                    this.Visit(u.Operand);
                    break;

                default:
                    throw new NotSupportedException(string.Format("The unary operator '{0}' is not supported", u.NodeType));
            }

            return u;
        }

        protected override Expression VisitBinary(BinaryExpression b)
        {
            _commandText += "(";

            this.Visit(b.Left);
            string left = BufferedText;

            this.Visit(b.Right);
            string right = BufferedText;

            switch (b.NodeType)
            {
                case ExpressionType.AndAlso:
                    _commandText += left + " AND " + right;
                    break;

                case ExpressionType.OrElse:
                    _commandText += left + " OR" + right;
                    break;

                case ExpressionType.Equal:
                    _commandText += left;
                    if (right == "NULL")
                    {
                        _commandText += " IS ";
                    }
                    else
                    {
                        _commandText += " = ";
                    }
                    _commandText += right;
                    break;

                case ExpressionType.NotEqual:
                    if (right == "NULL")
                    {
                        _commandText += "(NOT ";
                    }
                    _commandText += left;
                    if (right == "NULL")
                    {
                        _commandText += " IS ";
                    }
                    else
                    {
                        _commandText += " = ";
                    }
                    _commandText += right + ")";
                    break;

                case ExpressionType.LessThan:
                    _commandText += left + " < " + right;
                    break;

                case ExpressionType.LessThanOrEqual:
                    _commandText += left + " <= " + right;
                    break;

                case ExpressionType.GreaterThan:
                    _commandText += left + " > " + right;
                    break;

                case ExpressionType.GreaterThanOrEqual:
                    _commandText += left + " >= " + right;
                    break;

                default:
                    throw new NotSupportedException(string.Format("The binary operator '{0}' is not supported", b.NodeType));
            }

            _commandText += ")";

            return b;
        }

        protected override Expression VisitConstant(ConstantExpression c)
        {
            IQueryable query = c.Value as IQueryable;

            if (query != null)
            {
                _selectQuery.Fields(DataTypeInfo.GetInfo(query.ElementType).PropertyNames);
                _selectQuery.From(query.ElementType.Name);
            }
            else if (c.Value == null)
            {
                _commandText += "NULL";
            }
            else
            {
                switch (Type.GetTypeCode(c.Value.GetType()))
                {
                    case TypeCode.Object:
                        throw new NotSupportedException(string.Format("The Constant '{0}' is not supported", "Object"));

                    default:
                        _commandText += _selectQuery.Param(c.Value);
                        break;
                }
            }

            return c;
        }

        protected override Expression VisitMember(MemberExpression node)
        {
            switch (node.Expression.NodeType)
            {
                case ExpressionType.Constant:
                    ConstantExpression constant = node.Expression as ConstantExpression;
                    switch (node.Member.MemberType)
                    {
                        case MemberTypes.Field:
                            object fieldValue = constant.Value.GetType().GetField(node.Member.Name).GetValue(constant.Value);
                            Visit(Expression.Constant(fieldValue));
                            break;

                        case MemberTypes.Property:
                            object propertyValue = constant.Value.GetType().GetProperty(node.Member.Name).GetValue(constant.Value);
                            Visit(Expression.Constant(propertyValue));
                            break;

                        default:
                            throw new NotSupportedException(string.Format("The member '{0}' is not supported", node.Member.Name));
                    }
                    break;

                case ExpressionType.Parameter:
                    _commandText += node.Member.Name;
                    break;

                default:
                    throw new NotSupportedException(string.Format("The member '{0}' is not supported", node.Member.Name));
            }

            return node;
        }

    }
}
